/*******************************************************************************
 * Copyright 2009 DCSpectrometer - http://code.google.com/p/dcspectrometer 
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *     
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 *******************************************************************************/
package com.dcspectrometer;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import com.dcspectrometer.kmeansclustering.Cluster;
import com.dcspectrometer.kmeansclustering.KMeanClustering;

/** Performs image analysis */
public class ImageAnalyser {
  // static final int LED_WAVELENGTHS[] = { 375, 565, 660, 587, 468, 615, 626, 605, 630 };
  // static final String LED_NAMES[] = { "C1", "C2", "B1", "B2", "B3", "A1", "A2", "A3", "A4" };
  static final String LED_NAMES[] = { "D1", "D2", "C2", "C1", "B1", "B2", "B3", "B4", "B5", "B6",
      "B7", "A7", "A6", "A5", "A4", "A3", "A2", "A1" };
  // static final String LED_NAMES[] = { "C1", "C2", "B3", "B2", "B1", "A1", "A2", "A3", "A4" };
  static final String WAVELENGTH_COLUMN_NAME = "Wavelength";
  static final String DARK_COLUMN_NAME = "Dark";
  // static final int LED_BRIGHTNESS[] = { 2850, 630, 3000, 1900, 2600, 2500, 2500, 2500, 2175 };
  // static final int LED_LOCATION_VECTORS[][] = { { 59, -5 }, { -49, 69 }, { 51, -1 }, { 55, -3 },
  // { -105, 77 }, { 54, -2 }, { 57, -4 }, { 55, -4 } };
  static final int LED_LOCATION_VECTORS[][] = { { 55, 0 }, { 0, 68 }, { -55, 0 }, { 0, 68 },
      { 55, 0 }, { 55, 0 }, { 55, 0 }, { 55, 0 }, { 55, 0 }, { 55, 0 }, { 0, 68 }, { -55, 0 },
      { -55, 0 }, { -55, 0 }, { -55, 0 }, { -55, 0 }, { -55, 0 } };
  // static final int LED_LOCATION_VECTORS[][] = { { 60, 0 }, { -60, 75 }, { 60, 0 }, { 60, 0 }, {
  // -120, 75 },
  // { 60, 0 }, { 60, 0 }, { 60, 0 } };

  /*
   * int maximaLocations[][] = { { 1949, 1360 }, { 2008, 1355 }, { 1959, 1424 }, { 2010, 1423 }, {
   * 2065, 1420 }, { 1960, 1497 }, { 2014, 1495 }, { 2071, 1491 }, { 2126, 1487 } };
   */

  static final double PERCENT_ERROR = 0.20;
  static final double ROUNDING_ERROR = 6;

  static final int GRADIENT_GRID_SIZE = 3;

  static boolean BIT_MODE_16 = true;

  // Class cannot be instantiated
  private ImageAnalyser() {

  }

  /*
   * public static void getGridStats(BufferedImage image, int height, int width) {
   * RasterTileStatistics tileStats;
   * 
   * Minimum xMin = new Minimum(); Minimum yMin = new Minimum(); Maximum xMax = new Maximum();
   * Maximum yMax = new Maximum();
   * 
   * for (int i = 0; i < (image.getWidth() / width) - 1; i++) { for (int j = 0; j <
   * (image.getHeight() / height) - 1; j++) { tileStats = new RasterTileStatistics(image.getData(new
   * Rectangle(i * width, j * height, width, height)));
   * 
   * double[] average = tileStats.getMean(); double[] range = tileStats.getStandardDeviation(); for
   * (int k = 0; k < range.length; k++) { range[k] *= 1.5; // 1.5 standard deviations. }
   * 
   * tileStats = new RasterTileStatistics(image.getData(new Rectangle(i * width, j * height, width,
   * height)), average, range);
   * 
   * // System.out.println("Tile (" + i + ", " + j + ") info:"); // System.out.println("\t" +
   * tileStats.toString(ImageStatistics.RED_CHANNEL, "red")); // System.out.println("\t" +
   * tileStats.toString(ImageStatistics.GREEN_CHANNEL, "green")); // System.out.println("\t" +
   * tileStats.toString(ImageStatistics.BLUE_CHANNEL, "blue"));
   * 
   * int threshold = 50;
   * 
   * if (tileStats.getStandardDeviation()[0] > threshold || tileStats.getStandardDeviation()[1] >
   * threshold || tileStats.getStandardDeviation()[2] > threshold) { xMin.addValue(i * width);
   * xMax.addValue(((i + 1) * width) - 1); yMin.addValue(j * height); yMax.addValue(((j + 1) *
   * height) - 1); System.out.print(String.format("(%3d, %3d)", i, j));
   * System.out.println(RasterTileStatistics.getTileInfo(tileStats)); } } }
   * 
   * System.out.println(String.format("(%4d, %4d) - (%4d, %4d)", (int) xMin.getResults(), (int) yMin
   * .getResults(), (int) xMax.getResults(), (int) yMax.getResults())); }
   */

  public static HashMap<Point, int[]> getMaxima(BufferedImage image, ArrayList<Point> edges) {

    /** For each edge find maxima using gradient descent algorithm. */
    ArrayList<Point> associatedMaxima = new ArrayList<Point>();
    for (int i = 0; i < edges.size(); i++) {
      Point nextMaxima = findMaxima(image, edges.get(i));
      if (nextMaxima != null) {
        if (!associatedMaxima.contains(nextMaxima)) {
          associatedMaxima.add(nextMaxima);
          // System.out.println(String.format("[%d, %d]", nextMaxima.x, nextMaxima.y));
        }
      }
    }

    /** Group all maximas into clusters and obtain centroid locations. */

    KMeanClustering clustering = new KMeanClustering(LED_NAMES.length, associatedMaxima);

    ArrayList<Cluster> clusters = clustering.getClusters();

    // for (int i = 0; i < clusters.size(); i++) {
    // ArrayList<Point> nextClusterPoints = clusters.get(i).getPoints();
    // System.out.println("-----------Cluster " + i + "---------");
    // System.out.println("Centroid = [" + clusters.get(i).getCentroid().x + ","
    // + clusters.get(i).getCentroid().y + "]");
    // for (int j = 0; j < nextClusterPoints.size(); j++) {
    // Point nextPoint = nextClusterPoints.get(j);
    // System.out.println("[" + nextPoint.x + "," + nextPoint.y + "]");
    // }
    // }

    ArrayList<Point> centroids = new ArrayList<Point>();
    for (int i = 0; i < clusters.size(); i++) {
      Point2D.Double nextCentroid = clusters.get(i).getCentroid();
      centroids.add(new Point((int) nextCentroid.x, (int) nextCentroid.y));
    }

    // Temporary to check what maxima are picked up.
    try {
      BufferedImage detectedImage = ImageAnalyser.showPoints(image, centroids, 5, 5, Color.GREEN
          .getRGB());
      ImageLoader.saveTiff("/home/user/workspace/dcspectrometer/samples/" + "temp.TIFF",
          detectedImage);
    } catch (IOException e) {
      e.printStackTrace();
    }

    /** Construct Hashmap that contains maxima points and associated RGB data */
    HashMap<Point, int[]> maxima = new HashMap<Point, int[]>();
    /** Preset implementation, remove once gradient search is implemented. */
    /*
     * int maximaLocations[][] = { { 1949, 1360 }, { 2008, 1355 }, { 1959, 1424 }, { 2010, 1423 }, {
     * 2065, 1420 }, { 1960, 1497 }, { 2014, 1495 }, { 2071, 1491 }, { 2126, 1487 } };
     */

    int pointData[];
    for (int i = 0; i < centroids.size(); i++) {
      pointData = new int[3];

      Raster pixelRaster = image
          .getData(new Rectangle(centroids.get(i).x-2, centroids.get(i).y-2, 5, 5));
      RasterTileStatistics stats = new RasterTileStatistics(pixelRaster);
      //pixelRaster.getPixel(centroids.get(i).x, centroids.get(i).y, pointData);
      pointData[0] = (int) stats.getMean()[0];
      pointData[1] = (int) stats.getMean()[1];
      pointData[2] = (int) stats.getMean()[2];

      Point2D newPoint = new Point(centroids.get(i).x, centroids.get(i).y);
      /*
       * // Tests resiliance against rotation and scaling. AffineTransform rotateVectors =
       * AffineTransform.getRotateInstance(1); newPoint = rotateVectors.transform(new
       * Point(maximaLocations[i][0], maximaLocations[i][1]), null);
       * AffineTransform.getScaleInstance(5, 5).transform(newPoint, newPoint);
       */

      maxima.put(new Point((int) newPoint.getX(), (int) newPoint.getY()), pointData);
      /*
       * System.out.println("{" + maximaLocations[i][0] + ", " + maximaLocations[i][1] + "} is " +
       * newPoint);
       */
    }

    return maxima;
  }

  public static HashMap<String, int[]> getLEDMapping(Raster raster, HashMap<Point, int[]> maxima) {

    Point[] maximaLocations = maxima.keySet().toArray(new Point[0]);

    ArrayList<Point> matchedMaxima = null;
    ArrayList<Point> unmatchedMaxima = null;

    for (int i = 0; i < maximaLocations.length; i++) {
      for (int j = 0; j < maximaLocations.length; j++) {
        if (j == i)
          continue;

        matchedMaxima = new ArrayList<Point>();
        matchedMaxima.add(maximaLocations[i]);
        matchedMaxima.add(maximaLocations[j]);

        // System.out.println("Point1=" + maximaLocations[i] + ", Point2=" + maximaLocations[j]);

        unmatchedMaxima = new ArrayList<Point>(maxima.keySet());
        unmatchedMaxima.remove(maximaLocations[i]);
        unmatchedMaxima.remove(maximaLocations[j]);

        matchedMaxima = findMatchingOrder(matchedMaxima, unmatchedMaxima);

        if (matchedMaxima != null) {
          break;
        }
      }
      if (matchedMaxima != null) {
        break;
      }
    }

    if (matchedMaxima == null) {
      System.out.println("No match found");
      return null;
    }

    System.out.println("Match found");

    HashMap<String, int[]> mapping = new HashMap<String, int[]>();

    for (int i = 0; i < LED_NAMES.length; i++) {
      mapping.put(LED_NAMES[i], maxima.get(matchedMaxima.get(i)));
      // System.out.println(LED_WAVELENGTHS[i]);
    }

    return mapping;
  }

  private static ArrayList<Point> findMatchingOrder(ArrayList<Point> matchedMaxima,
      ArrayList<Point> unmatchedMaxima) {

    if (matchedMaxima.size() > LED_LOCATION_VECTORS.length) {
      return matchedMaxima;
    }

    double maximaDistanceScale = matchedMaxima.get(0).distance(matchedMaxima.get(1));
    Point firstMaximaVector = new Point((int) ((matchedMaxima.get(1).x - matchedMaxima.get(0).x)),
        (int) ((matchedMaxima.get(1).y - matchedMaxima.get(0).y)));
    double ledDistanceScale = Math.sqrt(LED_LOCATION_VECTORS[0][0] * LED_LOCATION_VECTORS[0][0]
        + LED_LOCATION_VECTORS[0][1] * LED_LOCATION_VECTORS[0][1]);
    Point firstLEDVector = new Point((int) (LED_LOCATION_VECTORS[0][0]),
        (int) (LED_LOCATION_VECTORS[0][1]));

    AffineTransform maximaRotation = AffineTransform.getRotateInstance(firstLEDVector.x,
        firstLEDVector.y);

    AffineTransform tempRotation = AffineTransform.getRotateInstance(firstMaximaVector.x,
        firstMaximaVector.y);

    try {
      tempRotation.invert();
    } catch (NoninvertibleTransformException e) {
      e.printStackTrace();
      return null;
    }

    maximaRotation.concatenate(tempRotation);

    /*
     * System.out.println("FirstLEDVector=" + firstLEDVector + ", FirstMaximaVector=" +
     * firstMaximaVector + ", TransformMtoL=" + maximaRotation.transform(firstMaximaVector, null) );
     * AffineTransform scale1 = AffineTransform.getScaleInstance(1/ledDistanceScale,
     * 1/ledDistanceScale); AffineTransform scale2 =
     * AffineTransform.getScaleInstance(1/maximaDistanceScale, 1/maximaDistanceScale);
     * System.out.println("FirstLEDVector=" + scale1.transform(firstLEDVector, null) +
     * ", FirstMaximaVector=" + scale2.transform(firstMaximaVector, null) + ", TransformMtoL=" +
     * scale2.transform(maximaRotation.transform(firstMaximaVector, null), null) );
     */

    Point lastPoint = matchedMaxima.get(matchedMaxima.size() - 1);

    for (int i = 0; i < unmatchedMaxima.size(); i++) {
      Point nextPoint = unmatchedMaxima.get(i);

      if (matchVectors(lastPoint, nextPoint, maximaDistanceScale, maximaRotation,
          LED_LOCATION_VECTORS[matchedMaxima.size() - 1], ledDistanceScale)) {
        // System.out.println(" = true");

        ArrayList<Point> nextMatched = duplicateArrayList(matchedMaxima);
        ArrayList<Point> nextUnmatched = duplicateArrayList(unmatchedMaxima);
        nextMatched.add(nextPoint);
        nextUnmatched.remove(nextPoint);

        nextMatched = findMatchingOrder(nextMatched, nextUnmatched);

        if (nextMatched != null) {
          return nextMatched;
        }
      }

      // System.out.println(" = false");
    }

    return null;
  }

  private static ArrayList<Point> duplicateArrayList(ArrayList<Point> list) {
    ArrayList<Point> newList = new ArrayList<Point>();
    for (int i = 0; i < list.size(); i++) {
      newList.add(list.get(i));
    }
    return newList;
  }

  private static boolean matchVectors(Point startPoint, Point endPoint, double maximaDistanceScale,
      AffineTransform maximaRotation, int[] ledVector, double ledDistanceScale) {

    /*
     * if (startPoint.x == 2007 && startPoint.y == 1350 && endPoint.x == 1961 && endPoint.y == 1422
     * && ledVector[0] == -49 && ledVector[1] == 69) { startPoint.getLocation(); }
     */

    Point2D newStartPoint = maximaRotation.transform(startPoint, null);
    Point2D newEndPoint = maximaRotation.transform(endPoint, null);

    Point2D rotatedMaximaVector = new Point2D.Double(newEndPoint.getX() - newStartPoint.getX(),
        newEndPoint.getY() - newStartPoint.getY());

    // System.out.print("LED{" + ledVector[0] + ", " + ledVector[1] + "} to " +
    // rotatedMaximaVector);

    if (!similarValues(rotatedMaximaVector.getX(), maximaDistanceScale, ledVector[0],
        ledDistanceScale)) {
      return false;
    }

    if (!similarValues(rotatedMaximaVector.getY(), maximaDistanceScale, ledVector[1],
        ledDistanceScale)) {
      return false;
    }

    return true;
  }

  private static boolean similarValues(double val1, double scale1, double val2, double scale2) {
    if (Math.abs((val1 * scale2) / (val2 * scale1) - 1) <= PERCENT_ERROR) {
      return true;
    }

    if (Math.abs((val1 * scale2 / scale1) - val2) <= ROUNDING_ERROR) {
      return true;
    }

    return false;
  }

  public static HashMap<Double, int[]> getDefaultWavelengthMapping() {
    HashMap<Double, int[]> wavelengthMaping = new HashMap<Double, int[]>();

    double mapping[][] = { { 375.0, 0.0, 0.0, 0.0 }, { 387.5, 0.2, 0.1, 1.0 },
        { 412.5, 0.7, 0.15, 3.5 }, { 437.5, 0.8, 0.1, 6.0 }, { 462.5, 0.9, 0.6, 7.0 },
        { 480.0, 0.8, 2.0, 6.5 }, { 505.0, 0.6, 3.3, 3.5 }, { 525.0, 1.0, 4.0, 0.0 },
        { 550.0, 0.7, 3.5, 0.0 }, { 575.0, 2.1, 1.0, 0.0 }, { 600.0, 3.3, 0.7, 0.0 },
        { 625.0, 3.3, 0.15, 0.15 }, { 650.0, 3.0, 0.1, 0.2 }, { 675.0, 2.8, 0.15, 0.3 },
        { 700.0, 2.6, 0.40, 0.2 }, { 712.5, 2.5, 0.50, 0.1 }, { 737.5, 2.6, 0.80, 0.0 },
        { 762.5, 2.5, 1.3, 0.0 }, { 775.0, 2.5, 1.4, 0.5 }, { 800.0, 2.4, 1.5, 2.4 },
        { 825.0, 2.2, 1.5, 2.6 }, { 850.0, 2.25, 1.5, 2.6 }, { 875.0, 2.2, 1.5, 2.6 },
        { 900.0, 1.5, 1.0, 1.9 }, { 925.0, 1.4, 0.85, 1.8 }, { 950.0, 1.3, 0.8, 1.7 },
        { 975.0, 1.15, 0.7, 1.6 }, { 1000.0, 1, 0.7, 1.4 } };

    double mappingScaleFactor = 7.0;

    int pointData[];
    for (int i = 0; i < mapping.length; i++) {
      pointData = new int[3];
      pointData[ImageStatistics.RED_CHANNEL] = (int) (mapping[i][1] * (Math.pow(2, 16) - 1) / mappingScaleFactor);
      pointData[ImageStatistics.GREEN_CHANNEL] = (int) (mapping[i][2] * (Math.pow(2, 16) - 1) / mappingScaleFactor);
      pointData[ImageStatistics.BLUE_CHANNEL] = (int) (mapping[i][3] * (Math.pow(2, 16) - 1) / mappingScaleFactor);
      wavelengthMaping.put(new Double(mapping[i][0]), pointData);
    }
    return wavelengthMaping;
  }

  private static Point findMaxima(BufferedImage image, Point startingPoint) {
    // System.out.println(String.format("[%d, %d]", startingPoint.x, startingPoint.y));
    double[][][] values = new double[3][3][3];
    double[][][] gradients = new double[3][3][3];

    int halfGrid = GRADIENT_GRID_SIZE / 2;

    /** Calculate 9 Point statistics */
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        Rectangle tile = new Rectangle(startingPoint.x - ((1 - i) * GRADIENT_GRID_SIZE + halfGrid),
            startingPoint.y - ((1 - j) * GRADIENT_GRID_SIZE + halfGrid), GRADIENT_GRID_SIZE,
            GRADIENT_GRID_SIZE);
        values[i][j] = new RasterTileStatistics(image.getData(tile)).getMean();
      }
    }

    /** Calculate Gradients */
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        for (int k = 0; k < 3; k++) {
          gradients[i][j][k] = values[i][j][k] - values[1][1][k];
        }
      }
    }

    /** Find Highest Gradient */
    int maxI = 1, maxJ = 1;
    double maxGradient = 0;
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        double gradientSum = 0;

        for (int k = 0; k < 3; k++) {
          gradientSum += gradients[i][j][k];
        }

        if (maxGradient < gradientSum) {
          maxGradient = gradientSum;
          maxI = i;
          maxJ = j;
        }
      }
    }

    Point nextPoint = new Point(startingPoint.x - ((1 - maxI) * GRADIENT_GRID_SIZE),
        startingPoint.y - ((1 - maxJ) * GRADIENT_GRID_SIZE));

    if (maxI == 1 && maxJ == 1) {
      return nextPoint;
    }

    /** Iterate on a higher gradient */
    return findMaxima(image, nextPoint);
  }

  public static BufferedImage showPoints(BufferedImage image, ArrayList<Point> points,
      int markWidth, int markHeight, int markRGB) {
    BufferedImage markedImage = new BufferedImage(image.getWidth(), image.getHeight(),
        BufferedImage.TYPE_INT_RGB);

    Raster nextRaster = image.getData();

    for (int x = 0; x < image.getWidth(); x++) {
      for (int y = 0; y < image.getHeight(); y++) {
        int nextColor = 0;
        if (BIT_MODE_16 == true) {
          nextColor = (new Color(nextRaster.getSample(x, y, 0) / 256,
              nextRaster.getSample(x, y, 1) / 256, nextRaster.getSample(x, y, 2) / 256)).getRGB();
        } else {
          nextColor = (new Color(nextRaster.getSample(x, y, 0), nextRaster.getSample(x, y, 1),
              nextRaster.getSample(x, y, 2))).getRGB();
        }
        markedImage.setRGB(x, y, nextColor);
      }
    }

    for (int i = 0; i < points.size(); i++) {
      Point nextPoint = points.get(i);
      int x = nextPoint.x - (markWidth / 2);
      int y = nextPoint.y - (markHeight / 2);
      for (int m = 0; m < markWidth; m++) {
        for (int n = 0; n < markHeight; n++) {
          markedImage.setRGB(x + m, y + n, markRGB);
        }
      }
    }

    return markedImage;
  }
}
